﻿using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;
using System.Diagnostics;
using System.Text;

namespace AutoRevit.Generator;

/// <summary>
/// 这是一个源代码生成器，用于为外部命令生成代码。
/// </summary>
[Generator]
public class ExternalCommandGenerator : ISourceGenerator
{
    private const string attributeText =
        $@"#pragma warning disable
#nullable enable

namespace AutoRevit;

[AttributeUsage(AttributeTargets.Method)]
[System.Diagnostics.Conditional(""AutoExternalCommandGenerator_DEBUG"")]
internal sealed class AutoExternalCommandAttribute : Attribute
{{

    public string? Namespace {{ get; set; }}

    public string? ClassName {{ get; set; }}

    public string? TransactionMode {{ get; set; }}

    public string? JournalingMode {{ get; set; }}

    public string? RegenerationOption {{ get; set; }}
}}";

    /// <summary>
    /// 在每次世代传递前按需创建
    /// </summary>
    private class SyntaxContextReceiver : ISyntaxContextReceiver
    {
        public List<IMethodSymbol> MethodSymbols { get; } = [];

        /// <summary>
        /// 在编译过程中针对每一个语法节点发出请求，我们能够检查这些节点并保存任何有助于生成过程的信息。
        /// </summary>
        public void OnVisitSyntaxNode(GeneratorSyntaxContext context)
        {
            if (context.Node is MethodDeclarationSyntax methodDeclarationSyntax && methodDeclarationSyntax.AttributeLists.Count > 0)
            {
                // 获取方法符号
                if (context.SemanticModel.GetDeclaredSymbol(methodDeclarationSyntax) is not IMethodSymbol methodSymbol)
                {
                    return;
                }
                // 检查方法是否具有特定属性
                if (methodSymbol.GetAttributes().Any(ad => ad.AttributeClass!.ToDisplayString() == "AutoRevit.AutoExternalCommandAttribute"))
                {
                    MethodSymbols.Add(methodSymbol);
                }
            }
        }
    }

    private static readonly DiagnosticDescriptor resultTypeDescriptor = new (id: "ARG006",
                                                                             title: "Invalid Method Symbol",
                                                                             messageFormat: "The method '{0}' does not meet the required conditions for code generation. The return type must be 'Autodesk.Revit.UI.Result'.",
                                                                             category: "AutoRevit.Generator",
                                                                             DiagnosticSeverity.Error,
                                                                             isEnabledByDefault: true);

    public void Initialize(GeneratorInitializationContext context)
    {
        context.RegisterForPostInitialization((i) =>
        {
            i.AddSource("AutoExternalCommandAttribute.g.cs", SourceText.From(attributeText, Encoding.UTF8));
        });

        context.RegisterForSyntaxNotifications(() => new SyntaxContextReceiver());
    }

    public void Execute(GeneratorExecutionContext context)
    {
        if (context.SyntaxContextReceiver is not SyntaxContextReceiver syntaxContextReceiver)
        {
            return;
        }

        INamedTypeSymbol attributeSymbol = context.Compilation.GetTypeByMetadataName("AutoRevit.AutoExternalCommandAttribute")!;

        INamedTypeSymbol externalCommandDataType = context.Compilation.GetTypeByMetadataName("Autodesk.Revit.UI.ExternalCommandData")!;
        INamedTypeSymbol stringType = context.Compilation.GetSpecialType(SpecialType.System_String);
        INamedTypeSymbol elementSetType = context.Compilation.GetTypeByMetadataName("Autodesk.Revit.DB.ElementSet")!;
        INamedTypeSymbol externalCommandParametersType = context.Compilation.GetTypeByMetadataName("AutoRevit.Entry.Command.IRevitData")!;
        INamedTypeSymbol returnType = context.Compilation.GetTypeByMetadataName("Autodesk.Revit.UI.Result")!;

        

        foreach (IMethodSymbol methodSymbol in syntaxContextReceiver.MethodSymbols)
        {

            int failedValidationCount = 0;
            // 检查方法符号是否符合条件
            if (!ReportDiagnostic.ValidIsStatic(context, methodSymbol))
            {
                failedValidationCount++;
            }

            if (!ReportDiagnostic.ValidNotReturnsVoid(context, methodSymbol))
            {
                failedValidationCount++;
            }

            if (!ReportDiagnostic.ValidIsPublicOrInternal(context, methodSymbol))
            {
                failedValidationCount++;
            }

            if (!SymbolEqualityComparer.Default.Equals(methodSymbol.ReturnType, returnType))
            {
                context.ReportDiagnostic(Diagnostic.Create(
                        resultTypeDescriptor,
                        methodSymbol.Locations.FirstOrDefault(),
                        methodSymbol.Name));

                failedValidationCount++;
            }

            if (failedValidationCount > 0)
            {
                continue;
            }

            string classSource = ProcessClass(methodSymbol,
                                              attributeSymbol,
                                              externalCommandDataType,
                                              stringType,
                                              elementSetType,
                                              externalCommandParametersType);

            Debug.WriteLine(classSource);

            context.AddSource($"{methodSymbol.Name}_AutoExternalCommand.g.cs", SourceText.From(classSource!, Encoding.UTF8));
        }
    }

    private string ProcessClass(IMethodSymbol methodSymbol,
                                INamedTypeSymbol attributeSymbol,
                                INamedTypeSymbol externalCommandDataType,
                                INamedTypeSymbol stringType,
                                INamedTypeSymbol elementSetType,
                                INamedTypeSymbol externalCommandParametersType)
    {
        string namespaceName = SymbolUtils.GetAttributePropertyValue(methodSymbol, attributeSymbol, "Namespace") ?? methodSymbol.ContainingNamespace.ToDisplayString();

        string className = SymbolUtils.GetAttributePropertyValue(methodSymbol, attributeSymbol, "ClassName") ?? $"{methodSymbol.Name}AutoExternalCommand";

        StringBuilder source = new($@"#pragma warning disable
#nullable enable

using {methodSymbol.ContainingNamespace};

namespace {namespaceName};");

        source.AppendLine();
        source.AppendLine();

        ProcessAttribute(methodSymbol, attributeSymbol, source);

        source.Append($@"public class {className} : global::Autodesk.Revit.UI.IExternalCommand
{{
    public global::Autodesk.Revit.UI.Result Execute(global::Autodesk.Revit.UI.ExternalCommandData commandData, ref string message, global::Autodesk.Revit.DB.ElementSet elements)
    {{
");
        bool isRevitData = methodSymbol.Parameters.Any(t => SymbolEqualityComparer.Default.Equals(externalCommandParametersType, t.Type));

        if (isRevitData)
        {
            source.AppendLine("        global::AutoRevit.Entry.Command.IRevitData data = global::AutoRevit.Entry.Command.RevitDataFactory.Create(commandData, elements);");
        }

        source.Append($"        global::Autodesk.Revit.UI.Result result = global::{methodSymbol.ContainingType}.{methodSymbol.Name}");

        ProcessParameter(methodSymbol,
                         externalCommandDataType,
                         stringType,
                         elementSetType,
                         externalCommandParametersType,
                         source);

        if (isRevitData)
        {
            source.AppendLine("        message = data.ResultMessage;");
        }

        source.AppendLine("        return result;");

        source.Append("    }\r\n}");

        return source.ToString();
    }

    private void ProcessAttribute(IMethodSymbol methodSymbol,
                                  INamedTypeSymbol attributeSymbol,
                                  StringBuilder source)
    {
        string transactionModeOptValue = SymbolUtils.GetAttributePropertyValue(methodSymbol, attributeSymbol, "TransactionMode") ?? "Manual";
        source.AppendLine($"[global::Autodesk.Revit.Attributes.Transaction(global::Autodesk.Revit.Attributes.TransactionMode.{transactionModeOptValue})]");

        string? JournalingModeOptValue = SymbolUtils.GetAttributePropertyValue(methodSymbol, attributeSymbol, "JournalingMode");

        if (JournalingModeOptValue is not null)
        {
            source.AppendLine($"[global::Autodesk.Revit.Attributes.Journaling(global::Autodesk.Revit.Attributes.JournalingMode.{JournalingModeOptValue})]");
        }

        string? RegenerationOptionOptValue = SymbolUtils.GetAttributePropertyValue(methodSymbol, attributeSymbol, "RegenerationOption");

        if (RegenerationOptionOptValue is not null)
        {
            source.AppendLine($"[global::Autodesk.Revit.Attributes.Regeneration(global::Autodesk.Revit.Attributes.RegenerationOption.{RegenerationOptionOptValue})]");
        }
    }

    private void ProcessParameter(IMethodSymbol methodSymbol,
                                  INamedTypeSymbol externalCommandDataType,
                                  INamedTypeSymbol stringType,
                                  INamedTypeSymbol elementSetType,
                                  INamedTypeSymbol externalCommandParametersType,
                                  StringBuilder source)
    {
        if (methodSymbol.Parameters.Length == 0)
        {
            source.Append("();\r");
        }
        else
        {
            source.Append("(");

            for (int i = 0; i < methodSymbol.Parameters.Length; i++)
            {
                IParameterSymbol parameterSymbol = methodSymbol.Parameters[i];

                if (SymbolEqualityComparer.Default.Equals(externalCommandDataType, parameterSymbol.Type))
                {
                    if (i == 0)
                    {
                        source.Append("commandData");
                    }
                    else
                    {
                        source.Append(", commandData");
                    }
                }
                else if (SymbolEqualityComparer.Default.Equals(stringType, parameterSymbol.Type))
                {
                    if (parameterSymbol.RefKind == RefKind.Ref)
                    {
                        if (i == 0)
                        {
                            source.Append("ref message");
                        }
                        else
                        {
                            source.Append(", ref message");
                        }
                    }
                }
                else if (SymbolEqualityComparer.Default.Equals(elementSetType, parameterSymbol.Type))
                {
                    if (i == 0)
                    {
                        source.Append("elements");
                    }
                    else
                    {
                        source.Append(", elements");
                    }
                }
                else if (SymbolEqualityComparer.Default.Equals(externalCommandParametersType, parameterSymbol.Type))
                {
                    if (i == 0)
                    {
                        source.Append("data");
                    }
                    else
                    {
                        source.Append(", data");
                    }
                }
            }

            source.Append(");\r");
        }
    }
}